/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */

import {Rule, SchematicContext, Tree} from '@angular-devkit/schematics';
import {Schema} from './schema';
import {
  argbFromHex,
  themeFromSourceColor,
  hexFromArgb,
  TonalPalette,
} from '@material/material-color-utilities';

// For each color tonal palettes are created using the following hue tones. The
// tonal palettes then get used to create the different color roles (ex.
// on-primary) https://m3.material.io/styles/color/system/how-the-system-works
const HUE_TONES = [0, 10, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100];
// Note: Some of the color tokens refer to additional hue tones, but this only
// applies for the neutral color palette (ex. surface container is neutral
// palette's 94 tone). https://m3.material.io/styles/color/static/baseline
const NEUTRAL_HUE_TONES = HUE_TONES.concat([4, 6, 12, 17, 22, 24, 87, 92, 94, 96]);

/**
 * Gets color tonal palettes generated by Material from the provided color.
 * @param color Color that represent primary to generate all the tonal palettes.
 * @returns Object with tonal palettes for each color
 */
function getMaterialTonalPalettes(color: string): {
  primary: TonalPalette;
  secondary: TonalPalette;
  tertiary: TonalPalette;
  neutral: TonalPalette;
  neutralVariant: TonalPalette;
  error: TonalPalette;
} {
  try {
    let argbColor = argbFromHex(color);
    const theme = themeFromSourceColor(argbColor, [
      {
        name: 'm3-theme',
        value: argbColor,
        blend: true,
      },
    ]);
    return theme.palettes;
  } catch (e) {
    throw new Error(
      'Cannot parse the specified color ' +
        color +
        '. Please verify it is a hex color (ex. #ffffff or ffffff).',
    );
  }
}

/**
 * Gets map of all the color tonal palettes from a specified color.
 * @param color Color that represent primary to generate the color tonal palettes.
 * @returns Map with the colors and their hue tones and values.
 */
function getColorTonalPalettes(color: string): Map<string, Map<number, string>> {
  const tonalPalettes = getMaterialTonalPalettes(color);
  const palettes: Map<string, Map<number, string>> = new Map();
  for (const [key, palette] of Object.entries(tonalPalettes)) {
    const paletteKey = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
    const tones = paletteKey === 'neutral' ? NEUTRAL_HUE_TONES : HUE_TONES;
    const colorPalette: Map<number, string> = new Map();
    for (const tone of tones) {
      const color = hexFromArgb(palette.tone(tone));
      colorPalette.set(tone, color);
    }
    palettes.set(paletteKey, colorPalette);
  }
  return palettes;
}

/**
 * Gets the scss representation of the provided color palettes.
 * @param colorPalettes Map of colors and their hue tones and values.
 * @returns String of the color palettes scss.
 */
function getColorPalettesSCSS(colorPalettes: Map<string, Map<number, string>>): string {
  let scss = '(\n';
  for (const [variant, palette] of colorPalettes!.entries()) {
    scss += '  ' + variant + ': (\n';
    for (const [key, value] of palette.entries()) {
      scss += '    ' + key + ': ' + value + ',\n';
    }
    scss += '  ),\n';
  }
  scss += ');';
  return scss;
}

/**
 * Gets the generated scss from the provided color palettes and theme types.
 * @param colorPalettes Map of colors and their hue tones and values.
 * @param themeTypes Theme types for the theme (ex. 'light', 'dark', or 'both').
 * @param colorComment Comment with original hex colors used to generate palettes.
 * @returns String of the generated theme scss.
 */
export function generateSCSSTheme(
  colorPalettes: Map<string, Map<number, string>>,
  themeTypes: string,
  colorComment: string,
): string {
  let scss = [
    "// This file was generated by running 'ng generate @angular/material:m3-theme'.",
    '// Proceed with caution if making changes to this file.',
    '',
    "@use 'sass:map';",
    "@use '@angular/material' as mat;",
    '',
    '// Note: ' + colorComment,
    '$_palettes: ' + getColorPalettesSCSS(colorPalettes),
    '',
    '$_rest: (',
    '  secondary: map.get($_palettes, secondary),',
    '  neutral: map.get($_palettes, neutral),',
    '  neutral-variant: map.get($_palettes,  neutral-variant),',
    '  error: map.get($_palettes, error),',
    ');',
    '$_primary: map.merge(map.get($_palettes, primary), $_rest);',
    '$_tertiary: map.merge(map.get($_palettes, tertiary), $_rest);',
    '',
  ];

  let themes = themeTypes === 'both' ? ['light', 'dark'] : [themeTypes];
  // Note: Call define-theme function here since creating the color tokens
  // from the palettes is a private function
  for (const themeType of themes) {
    scss = scss.concat([
      '$' + themeType + '-theme: mat.define-theme((',
      '  color: (',
      '    theme-type: ' + themeType + ',',
      '    primary: $_primary,',
      '    tertiary: $_tertiary,',
      '  )',
      '));',
    ]);
  }
  return scss.join('\n');
}

/**
 * Creates theme file for provided scss.
 * @param scss scss for the theme file.
 * @param tree Directory tree.
 * @param directory Directory path to place generated theme file.
 */
function createThemeFile(scss: string, tree: Tree, directory?: string) {
  const filePath = directory ? directory + 'm3-theme.scss' : 'm3-theme.scss';
  tree.create(filePath, scss);
}

export default function (options: Schema): Rule {
  return async (tree: Tree, context: SchematicContext) => {
    const colorPalettes = getColorTonalPalettes(options.primaryColor);
    let colorComment = 'Color palettes are generated from primary: ' + options.primaryColor;

    if (options.secondaryColor) {
      colorPalettes.set('secondary', getColorTonalPalettes(options.secondaryColor).get('primary')!);
      colorComment += ', secondary: ' + options.secondaryColor;
    }
    if (options.tertiaryColor) {
      colorPalettes.set('tertiary', getColorTonalPalettes(options.tertiaryColor).get('primary')!);
      colorComment += ', tertiary: ' + options.tertiaryColor;
    }
    if (options.neutralColor) {
      colorPalettes.set('neutral', getColorTonalPalettes(options.neutralColor).get('primary')!);
      colorComment += ', neutral: ' + options.neutralColor;
    }

    if (!options.themeTypes) {
      context.logger.info('No theme types specified, creating both light and dark themes.');
      options.themeTypes = 'both';
    }

    const themeScss = generateSCSSTheme(colorPalettes, options.themeTypes, colorComment);
    createThemeFile(themeScss, tree, options.directory);
  };
}
